using System;
using System.Collections.Generic;
using System.Text;

namespace UserInterface.MML.Base
{
    internal class Scanner
    {
        private static List<string> allowedTagNames;

        internal static List<string> AllowedTagNames
        {
            get { return allowedTagNames; }
            set { allowedTagNames = value; }
        }

        private static bool IsTagAllowed(string name)
        {
            return allowedTagNames == null
                || allowedTagNames.Contains(name);
        }
        
        private enum State
        {
            Start,
            InStartTag,
            InEndTag,
            InAttributeList,
            InAttributeName,
            InValue
        }

        private char[] input;
        private int index = 0;
        private State state = State.Start;
        private StringBuilder buffer = new StringBuilder();
        private List<Token> tokens = new List<Token>();

        public Scanner(string input)
        {
            this.input = input.ToCharArray();
            PerformScan();
        }

        public List<Token> Tokens
        {
            get { return tokens; }
        }

        private void ClearBuffer()
        {
            if (buffer.Length > 0)
            {
                buffer = new StringBuilder();
            }
        }

        private void AddBufferAsCharacterDataToken()
        {
            if (buffer.Length > 0)
            {
                string data = buffer.ToString();
                CharacterDataToken lastToken = null;
                if (tokens.Count > 0)
                {
                    int lastIndex = tokens.Count - 1;
                    lastToken = tokens[lastIndex] as CharacterDataToken;
                }
                if (lastToken == null)
                {
                    tokens.Add(new CharacterDataToken(data));
                }
                else
                {
                    lastToken.Data += data;
                    lastToken.Code += data;
                }
                ClearBuffer();
            }
        }

        private void CutBufferToLength(int length)
        {
            buffer = new StringBuilder(
                buffer.ToString().Substring(0, length)
            );
        }

        private void ScanError()
        {
            AddBufferAsCharacterDataToken();
            state = State.Start;
        }

        private void PerformScan()
        {
            while (index < input.Length)
            {
                switch (state)
                {
                    case State.Start:
                        HandleStartState();
                        break;
                    case State.InStartTag:
                        HandleInStartTagStartState();
                        break;
                    case State.InAttributeList:
                        HandleInAttributeListState();
                        break;
                    case State.InAttributeName:
                        HandleInAttributeNameState();
                        break;
                    case State.InValue:
                        HandleInValueState();
                        break;
                    case State.InEndTag:
                        HandleInEndTagState();
                        break;
                }
            }
            AddBufferAsCharacterDataToken();
        }

        private void HandleStartState()
        {
            char currentChar = input[index];
            if (currentChar == '[')
            {
                AddBufferAsCharacterDataToken();
                buffer.Append('[');
                index++;
                if (index >= input.Length)
                {
                    ScanError();
                }
                else
                {
                    currentChar = input[index];
                    if (currentChar == '/')
                    {
                        buffer.Append('/');
                        index++;
                        state = State.InEndTag;
                    }
                    else
                    {
                        state = State.InStartTag;
                    }
                }
            }
            else
            {
                buffer.Append(currentChar);
                index++;
            }
        }

        private void HandleInStartTagStartState()
        {
            int startIndex = index;
            int startBufferLength = buffer.Length;
            StringBuilder nameBuilder = new StringBuilder();
            int localIndex = index;
            bool hadNamePart = false;
            while (true)
            {
                if (localIndex >= input.Length)
                {
                    index = startIndex;
                    CutBufferToLength(startBufferLength);
                    state = State.Start;
                    break;
                }
                char currentChar = input[localIndex];
                bool isNamePart = false;
                bool forceAddToBuffer = false;
                bool isEndChar = false;
                bool isError = false;
                if (Char.IsLetterOrDigit(currentChar))
                {
                    isNamePart = true;
                }
                else if (Char.IsWhiteSpace(currentChar))
                {
                    if (nameBuilder.Length == 0)
                    {
                        forceAddToBuffer = true;
                    }
                    else
                    {
                        isEndChar = true;
                    }
                }
                else if (currentChar == ']')
                {
                    isEndChar = true;
                }
                else if (currentChar == '=')
                {
                    if (hadNamePart)
                    {
                        isEndChar = true;
                    }
                    else
                    {
                        isError = true;
                    }
                }
                else
                {
                    isError = true;
                }
                if (forceAddToBuffer || isNamePart)
                {
                    buffer.Append(currentChar);
                    localIndex++;
                }
                if (isNamePart)
                {
                    nameBuilder.Append(currentChar);
                    hadNamePart = true;
                }
                if (isEndChar)
                {
                    string name = nameBuilder.ToString();
                    if (IsTagAllowed(name))
                    {
                        tokens.Add(
                            new StartTagStartToken(
                                name,
                                buffer.ToString()
                            )
                        );
                        ClearBuffer();
                        state = State.InAttributeList;
                        index = localIndex;
                        break;
                    }
                    else
                    {
                        isError = true;
                    }
                }
                if (isError)
                {
                    index = localIndex;
                    ScanError();
                    break;
                }
            }
        }

        private void HandleInAttributeListState()
        {
            char currentChar = input[index];
            if (Char.IsWhiteSpace(currentChar))
            {
                buffer.Append(currentChar);
                index++;
            }
            else if (currentChar == '=')
            {
                buffer.Append(currentChar);
                index++;
                tokens.Add(new AssignmentToken(buffer.ToString()));
                ClearBuffer();
                state = State.InValue;
            }
            else if (currentChar == ']')
            {
                buffer.Append(currentChar);
                index++;
                tokens.Add(new StartTagEndToken(buffer.ToString()));
                ClearBuffer();
                state = State.Start;
            }
            else if (Char.IsLetterOrDigit(currentChar))
            {
                state = State.InAttributeName;
            }
            else
            {
                ScanError();
            }
        }

        private void HandleInAttributeNameState()
        {
            int startIndex = index;
            int startBufferLength = buffer.Length;
            StringBuilder nameBuilder = new StringBuilder();
            int localIndex = index;
            while (true)
            {
                if (localIndex >= input.Length)
                {
                    index = startIndex;
                    CutBufferToLength(startBufferLength);
                    state = State.Start;
                    break;
                }
                char currentChar = input[localIndex];
                bool isNameChar = false;
                bool forceAddToBuffer = false;
                bool isEndChar = false;
                if (Char.IsLetterOrDigit(currentChar))
                {
                    isNameChar = true;
                }
                else if (Char.IsWhiteSpace(currentChar))
                {
                    if (nameBuilder.Length == 0)
                    {
                        forceAddToBuffer = true;
                    }
                    else
                    {
                        isEndChar = true;
                    }
                }
                else if (currentChar == '=' || currentChar == ']')
                {
                    isEndChar = true;
                }
                else
                {
                    index = localIndex;
                    ScanError();
                    break;
                }
                if (forceAddToBuffer || isNameChar)
                {
                    buffer.Append(currentChar);
                    localIndex++;
                }
                if (isNameChar)
                {
                    nameBuilder.Append(currentChar);
                }
                if (isEndChar)
                {
                    if (buffer.Length > 0)
                    {
                        tokens.Add(
                            new AttributeNameToken(
                                nameBuilder.ToString(),
                                buffer.ToString()
                            )
                        );
                        ClearBuffer();
                    }
                    state = State.InAttributeList;
                    index = localIndex;
                    break;
                }
            }
        }

        private void HandleInValueState()
        {
            int startIndex = index;
            int startBufferLength = buffer.Length;
            StringBuilder valueBuilder = new StringBuilder();
            int localIndex = index;
            bool isDoubleQuoteOpen = false;
            bool isSingleQuoteOpen = false;
            while (true)
            {
                if (localIndex >= input.Length)
                {
                    index = startIndex;
                    CutBufferToLength(startBufferLength);
                    state = State.Start;
                    break;
                }
                char currentChar = input[localIndex];
                bool isValuePart = false;
                bool isEndOfValue = false;
                bool forceAddToBuffer = false;
                if (currentChar == ']')
                {
                    if (isSingleQuoteOpen || isDoubleQuoteOpen)
                    {
                        isValuePart = true;
                    }
                    else
                    {
                        isEndOfValue = true;
                    }
                }
                else if (Char.IsWhiteSpace(currentChar))
                {
                    if (isSingleQuoteOpen || isDoubleQuoteOpen)
                    {
                        isValuePart = true;
                    }
                    else if (valueBuilder.Length == 0)
                    {
                        forceAddToBuffer = true;
                    }
                    else
                    {
                        isEndOfValue = true;
                    }
                }
                else if (currentChar == '\'')
                {
                    forceAddToBuffer = true;
                    if (isSingleQuoteOpen)
                    {
                        isEndOfValue = true;
                    }
                    else
                    {
                        isSingleQuoteOpen = true;
                    }
                }
                else if (currentChar == '"')
                {
                    forceAddToBuffer = true;
                    if (isDoubleQuoteOpen)
                    {
                        isEndOfValue = true;
                    }
                    else
                    {
                        isDoubleQuoteOpen = true;
                    }
                }
                else
                {
                    isValuePart = true;
                }
                if (isValuePart || forceAddToBuffer)
                {
                    buffer.Append(currentChar);
                    localIndex++;
                }
                if (isValuePart)
                {
                    valueBuilder.Append(currentChar);
                }
                if (isEndOfValue)
                {
                    tokens.Add(
                        new ValueToken(
                            valueBuilder.ToString(),
                            buffer.ToString()
                        )
                    );
                    ClearBuffer();
                    index = localIndex;
                    state = State.InAttributeList;
                    break;
                }
            }
        }

        private void HandleInEndTagState()
        {
            int startIndex = index;
            int startBufferLength = buffer.Length;
            StringBuilder name = new StringBuilder();
            int localIndex = index;
            while (true)
            {
                if (localIndex >= input.Length)
                {
                    index = startIndex;
                    CutBufferToLength(startBufferLength);
                    state = State.Start;
                    break;
                }
                char currentChar = input[localIndex];
                localIndex++;
                buffer.Append(currentChar);
                if (Char.IsLetterOrDigit(currentChar))
                {
                    name.Append(currentChar);
                }
                else if (currentChar == ']')
                {
                    tokens.Add(
                        new EndTagToken(
                            name.ToString(),
                            buffer.ToString()
                        )
                    );
                    ClearBuffer();
                    state = State.Start;
                    index = localIndex;
                    break;
                }
                else if (!Char.IsWhiteSpace(currentChar))
                {
                    index = localIndex;
                    ScanError();
                    break;
                }
            }
        }
    }
}
